﻿//Copyright (c) 2010, Andreas Grünwald
//All rights reserved.

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text;
using Slf;
using yaTDL.Core.IO;
using yaTDL.Core.Properties;
using yaTDL.Extensions;
using yaTDL.Core.MVVM;

namespace yaTDL.Core.Toodledo
{
    public class ToodledoSyncServiceV1: Service
    {

        static ApplicationSettings Settings = ApplicationSettings.Settings;

        /// <summary>
        /// Initializes a new instance of ToodledoSyncService.
        /// </summary>
        public ToodledoSyncServiceV1()
        {
            setTimer();
            timer.Enabled = Settings.Toodledo_EnableSync;
            Settings.PropertyChanged += new PropertyChangedEventHandler(Settings_PropertyChanged);

            toodledoClient.ErrorOccurred += new EventHandler<ErrorEventArgsV1>(toodledoClient_ErrorOccured);
        }



        /// <summary>
        /// A event that will be raised if conflicting changes are found during sync.
        /// </summary>
        public event EventHandler<SyncConflictEventArgs> SyncConflictFound;



        /// <summary>
        /// The Logger used to log the synchronisations.
        /// </summary>
        ILogger syncLogger = LoggerService.GetLogger("ToodledoSync");

        /// <summary>
        /// The instance of ToodledoClient to interact with the Toodledo API.
        /// </summary>
        ToodledoClientV1 toodledoClient = new ToodledoClientV1();

        /// <summary>
        /// The currently used APIKey necessary for all methos of ToodledoClient.
        /// </summary>
        ToodledoAPIKey apiKey = new ToodledoAPIKey();

        /// <summary>
        /// The currently used ToodledoAccountInfo.
        /// </summary>
        ToodledoAccountInfoV1 accountInfo = new ToodledoAccountInfoV1();

        /// <summary>
        /// The instance of XMLFileaccess used to read/write cached data
        /// </summary>
        XMLFileHandler xmlFileaccess = new XMLFileHandler();

        /// <summary>
        /// Indicates whether there is currently a sync in progress; sync() will exit if true.
        /// </summary>
        static bool syncRunning = false;

        /// <summary>
        /// Stores whether there was a error raised by toodledoClient. If set to true, a Sync will end prematurely.
        /// </summary>
        bool errorOccurred = false;

        string lastErrorMessage;

        /// <summary>
        /// The Date and Time of the last synchorinsation.
        /// </summary>
        private DateTime lastSync { get { return Settings.Toodledo_LastSync; } }
     
        /// <summary>
        /// The Name of the file that stores the cached folder-list from the time of the last sync.
        /// </summary>
        private static string folderCacheLastSyncFileName { get { return Path.Combine(XMLFileHandler.DefaultDirectory, "Sync_folderCache.xml"); } }



        /// <summary>
        /// Executes the Service's task.
        /// </summary>
        protected override void DoAction()
        {
            sync();
        }

        /// <summary>
        /// Sets the timer that will start sync based on Settings.Default.Toodledo_SyncInterval.
        /// </summary>
        private void setTimer()
        {
            bool isEnabled = timer.Enabled;
            timer.Enabled = false;

            if ((DateTime.Now - Settings.Toodledo_LastSync).TotalMilliseconds > Settings.Toodledo_SyncInterval.TotalMilliseconds)
            {
                timer.Interval = new TimeSpan(0, 1, 0).TotalMilliseconds;
            }
            else
            {
                timer.Interval = Settings.Toodledo_SyncInterval.TotalMilliseconds;
            }

            timer.Enabled = isEnabled;
        }

        /// <summary>
        /// Listens for changes in the Settings and adjustes ToodledoSyncService if necessary.
        /// </summary>
        /// <param name="sender">The sender of the event.</param>
        /// <param name="e">The System.ComponentModel.PropertyChangedEventArgs containing more information about the event.</param>
        private void Settings_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "Toodledo_SyncInterval" || e.PropertyName == "Toodledo_LastSync" || e.PropertyName == "Toodledo_EnableSync")
            {
                setTimer();
            }
            if (e.PropertyName == "Toodledo_EnableSync")
            {
                timer.Enabled = Settings.Toodledo_EnableSync;
            }
        }

        /// <summary>
        /// Handles the ToodledoClient.ErrorOccured event. It will be logged and running syncs will be stopped.
        /// </summary>
        /// <param name="sender">The sender of the event.</param>
        /// <param name="e">EventArgs containing mor information about the error.</param>
        void toodledoClient_ErrorOccured(object sender, ErrorEventArgsV1 e)
        {
            errorOccurred = true;                                                   //will stop running syncs.
            lastErrorMessage = e.Error;
            syncLogger.Error("An Error occured in ToodledoClient: {0}", e.Error);   //logs the errror in the sync-log.
        }
        


        /// <summary>
        /// Sync all local data with Toodledo.
        /// </summary>
        private void sync()
        {
            if (!Settings.Toodledo_EnableSync || syncRunning) { return; }   //exit if Syncing is not enabled in the Settings or there is already a sync running.

            errorOccurred = false;                                  //make sure the value of errorOccurred is false. If it is set to true, the Syncing might exit prematurely.

            syncLogger.Info("Sync started at {0}, RequestRatio {1}", DateTime.Now, toodledoClient.RequestRatio);


            if (String.IsNullOrEmpty(apiKey.Key) || apiKey.Expires < DateTime.Now)  //checks if there is a APIKey and if it is still valid
            {
                //if the APIKey cannot be used any more, a new one will be requested.
                if (!Settings.Toodledo_UserId.IsNullOrEmpty() && Settings.Toodledo_Password.IsNullOrEmpty())
                {
                    syncLogger.Info("Requesting APIKey");
                    apiKey = toodledoClient.GetKey(Settings.Toodledo_UserId.Unwrap(), Settings.Toodledo_Password.Unwrap());
                    if (errorOccurred) { logExitSync(); return; }
                }
                else
                { if (errorOccurred) { logExitSync(); return; } }
            }
            

            syncProjects();     //syncs Projects/Folders        
            { if (errorOccurred) { logExitSync(); return; } }

            syncTasks();        //syncs Tasks
     

            Settings.Toodledo_LastSync = DateTime.Now;      //saves the Date and Time of the Sync
            Settings.Save();

            syncLogger.Info("Sync Completed, RequestRatio {0}", toodledoClient.RequestRatio);

        }

        /// <summary>
        /// Syncs local Projects with Folders on Toodledo.
        /// </summary>
        private void syncProjects()
        {
            syncLogger.Info("Syncing Projects");

            Dictionary<String, Project> toodledoFolders;
            Dictionary<String, Project> toodledoFolders_LastSync;
            List<string> toodledoModified;
            List<string> toodledoDeleted;
            Collection<string> localDeleted;

            syncLogger.Info("Downloading AccountInfo");
            accountInfo = toodledoClient.GetAccountInfo(apiKey.Key);
            if (errorOccurred) { logExitSync(); return; }

            syncLogger.Info("Downloading Projects");
            toodledoFolders = toodledoClient.GetFolders(apiKey.Key).ToDictionary<Project, string>(x => x.Id);
            if (errorOccurred) { logExitSync(); return; }

            toodledoFolders_LastSync = xmlFileaccess.GetProjects(folderCacheLastSyncFileName).ToDictionary<Project, string>(x => x.Id);


            toodledoDeleted = (from folder in toodledoFolders_LastSync.Values
                                      where !toodledoFolders.ContainsKey(folder.Id)
                                      select folder.Id).ToList<String>();

            foreach (string s in toodledoDeleted) { syncLogger.Info("Project deleted on Toodledo; Id:{0}", s); }         


            toodledoModified = new List<string>();
            foreach (Project p in toodledoFolders.Values)
            {
                if (toodledoFolders_LastSync.ContainsKey(p.Id))
                {
                    if (p.Diff(toodledoFolders_LastSync[p.Id]).Any())
                    { 
                        toodledoModified.Add(p.Id);
                        syncLogger.Info("Project modified on Toodledo; Id:{0}", p.Id);
                    }
                }
            }

            localDeleted = TaskStore.GetDeletedProjects(lastSync);
            foreach (string s in localDeleted) { syncLogger.Info("Project deleted locally; Id:{0}", s); } 

            //Projects deleted locally
            foreach (string id in localDeleted)
            {
                if (!toodledoModified.Contains(id) && !toodledoDeleted.Contains(id) && !id.StartsWith("l_p"))
                {
                    syncLogger.Info("Deleting Project on Toodledo; Id:{0}", id);
                    toodledoClient.DeleteFolder(apiKey.Key, id);
                    if (errorOccurred) { logExitSync(); return; }
                }
            }


            foreach (Project p in TaskStore.GetProjects())
            {
                bool modifiedLocally = p.LastEdited > lastSync ? true : false;

                if (toodledoFolders.ContainsKey(p.Id))
                {
                    Project p_toodledo = toodledoFolders[p.Id];

                    bool modifiedOnline = toodledoModified.Contains(p.Id) ? true : false;

                    if (modifiedLocally || modifiedOnline) syncLogger.Info("Project, Name: {0}, Id:{1}, modifiedLocally: {2}, modifiedOnline: {3}", p.Name, p.Id, modifiedLocally, modifiedOnline);

                    if (modifiedLocally && modifiedOnline)
                    {

                        if (p.Diff(p_toodledo).Any())
                        {

                            List<object> items = new List<object>();
                            items.Add(p_toodledo);
                            items.Add(p);

                            SyncConflictViewModel viewModel = new SyncConflictViewModel(items);
                            ViewManager.ShowView(viewModel);

                            if (!ViewManager.HasRegisteredView(typeof(SyncConflictViewModel)))
                            {
                                viewModel.SelectedItem = p;                               
                            }

                            Project p_resolved = viewModel.SelectedItem as Project;

                            syncLogger.Info("Updating Project locally; Id: {0}", p.Id);

                            p.Collapsed = p_resolved.Collapsed;
                            p.Indent = p_resolved.Indent;
                            p.Order = p_resolved.Order;
                            p.UserId = p_resolved.UserId;
                            p.Name = p_resolved.Name;
                            p.Color = p_resolved.Color;

                            p_toodledo.Collapsed = p_resolved.Collapsed;
                            p_toodledo.Indent = p_resolved.Indent;
                            p_toodledo.Order = p_resolved.Order;
                            p_toodledo.UserId = p_resolved.UserId;
                            p_toodledo.Name = p_resolved.Name;
                            p_toodledo.Color = p_resolved.Color;


                            syncLogger.Info("Updating Project on Toodledo; Id: {0}", p.Id);
                            toodledoClient.UpdateFolder(apiKey.Key, p);
                            if (errorOccurred) { logExitSync(); return; }
                        }
                    }
                    else
                    {
                        if (modifiedLocally)
                        {
                            syncLogger.Info("Updating Project on Toodledo; Id: {0}", p.Id);
                            toodledoClient.UpdateFolder(apiKey.Key, p);
                            if (errorOccurred) { logExitSync(); return; }
                        }
                        if (modifiedOnline)
                        {
                            syncLogger.Info("Updating Project locally; Id: {0}", p.Id);
                            p.Collapsed = p_toodledo.Collapsed;                            
                            p.Indent = p_toodledo.Indent;
                            p.Order = p_toodledo.Order;
                            p.UserId = p_toodledo.UserId;
                            p.Name = p_toodledo.Name;
                            p.Color = p_toodledo.Color;
                        }
                    }
                }
                else
                {
                    if (!toodledoDeleted.Contains(p.Id))
                    {
                        syncLogger.Info("Adding Project to Toodledo; Id: {0}", p.Id);
                        toodledoClient.AddFolder(apiKey.Key, p);
                        if (errorOccurred) 
                        {

                            if (lastErrorMessage == "That folder already exists")
                            {
                                var queryForExistingProject = from project in toodledoFolders.Values
                                                              where project.Name == p.Name
                                                              select project.Id;
                                                              
                                if (queryForExistingProject.Any())
                                {
                                    string existingId = queryForExistingProject.First();
                                    syncLogger.Warn(String.Format("Project's already existed on Toodledo, assigning existing Id from Toodledo {0} to local Project {1}", existingId, p.Id));
                                    p.Id = existingId;
                                }
                            }
                            else
                            {
                                logExitSync(); return;
                            }
                        }
                    }
                    else
                    {                        
                        if (modifiedLocally) 
                        {
                            syncLogger.Info("Readding Project to Toodledo (deleted but modified locally); Id: {0}", p.Id);
                            toodledoClient.AddFolder(apiKey.Key, p); 
                            if (errorOccurred) { logExitSync(); return; }
                        }
                        else 
                        {
                            syncLogger.Info("Deleting Project locally; Id: {0}", p.Id);
                            TaskStore.RemoveProject(p.Id);
                        }
                    }
                }
            }

            //remaining: Projects/Folders added online
            var query = from folder in toodledoFolders.Values
                        where !TaskStore.ProjectExists(folder.Id) && !localDeleted.Contains(folder.Id)
                        select folder;

            List<Project> addedFolders = query.ToList<Project>();

            foreach (Project p in addedFolders)
            {
                syncLogger.Info("Adding Project locally, Id: {0}", p.Id);
                TaskStore.AddProject(p);
            }

            //save the current list of Folders on Toodledo for the next sync
            xmlFileaccess.SaveData(toodledoClient.GetFolders(apiKey.Key), null, null, folderCacheLastSyncFileName);
        }

        /// <summary>
        /// Syncs local Tasks with Toodledo.
        /// </summary>
        private void syncTasks()
        {
            syncLogger.Info("Syncing Tasks");

            Dictionary<String, Task> toodledoTasksDict;
            List<Parameter> searchPatameters = new List<Parameter>();
            //searchPatameters.Add(new Parameter() { Name = "modafter", Value = lastSync });
            syncLogger.Info("Downloading Tasks, lastSync:{0}", lastSync);
            toodledoTasksDict = toodledoClient.GetTasks(apiKey.Key, searchPatameters).ToDictionary<Task, String>(x => x.Id);
            if (errorOccurred) { logExitSync(); return; }
            
            Collection<string> toodledoDeleted = toodledoClient.GetDeletedTasks(apiKey.Key, lastSync);
            if (errorOccurred) { logExitSync(); return; }
            foreach (string s in toodledoDeleted){ syncLogger.Info("Task deleted on Toodledo; Id: {0}", s); }            


            Collection<string> localDeleted = TaskStore.GetDeletedTasks(lastSync.AddMinutes(-1));
            foreach (string s in localDeleted) { syncLogger.Info("Task deleted locally; Id: {0}", s); }   

            //Tasks deleted locally
            foreach (string id in localDeleted)
            {
                if (toodledoTasksDict.ContainsKey(id) && !toodledoDeleted.Contains(id))
                {
                    if (toodledoTasksDict[id].LastEdited > lastSync.AddMinutes(-1))
                    {
                        syncLogger.Info("Readding Task locally (deleted, but modified online); Id: {0}", id);
                        TaskStore.AddTask(toodledoTasksDict[id]);
                    }
                    else
                    {
                        syncLogger.Info("Deleting Task on Toodledo; Id: {0}", id); 
                        toodledoClient.DeleteTask(apiKey.Key, id);
                        if (errorOccurred) { logExitSync(); return; }
                    }
                }
            }


            foreach (Task t in TaskStore.GetTasks())
            {
                bool modifiedLocally = t.LastEdited > lastSync ? true : false;

                if (toodledoTasksDict.ContainsKey(t.Id))
                {                  
                    Task t_toodledo = toodledoTasksDict[t.Id];
                    
                    bool modifiedOnline = t_toodledo.LastEdited > lastSync ? true : false; 

                    if (modifiedLocally || modifiedOnline) syncLogger.Info("Task, Content: {0}, Id:{1}, modifiedLocally: {2}, modifiedOnline: {3}", t.Content, t.Id, modifiedLocally, modifiedOnline);

                    if (modifiedLocally && modifiedOnline)
                    {                       
                        if (t.Diff(t_toodledo).Any())
                        {

                            List<object> items = new List<object>();
                            items.Add(t_toodledo);
                            items.Add(t);

                            SyncConflictViewModel syncConflictViewModel = new SyncConflictViewModel(items);
                            ViewManager.ShowView(syncConflictViewModel);

                            if (!ViewManager.HasRegisteredView(typeof(SyncConflictViewModel)))
                            {
                                if (t.LastEdited >= t_toodledo.LastEdited)
                                {
                                    syncConflictViewModel.SelectedItem = t;                                    
                                }
                                else
                                { syncConflictViewModel.SelectedItem = t_toodledo; }
                            }

                            Task t_resolved = syncConflictViewModel.SelectedItem as Task;

                            syncLogger.Info("Updating Task locally; Id: {0}", t.Id);
                            t.Collapsed = t_resolved.Collapsed;
                            t.Content = t_resolved.Content;
                            t.DueDate = t_resolved.DueDate;
                            t.Indent = t_resolved.Indent;
                            t.Completed = t_resolved.Completed;
                            t.Order = t_resolved.Order;
                            t.Priority = t_resolved.Priority;
                            t.ProjectId = t_resolved.ProjectId;
                            t.Recurrence.AdvancedRepeat = t_resolved.Recurrence.AdvancedRepeat;
                            t.Recurrence.Repeat = t_resolved.Recurrence.Repeat;
                            t.UserId = t_resolved.UserId;

                            t_toodledo.Collapsed = t_resolved.Collapsed;
                            t_toodledo.Content = t_resolved.Content;
                            t_toodledo.DueDate = t_resolved.DueDate;
                            t_toodledo.Indent = t_resolved.Indent;
                            t_toodledo.Completed = t_resolved.Completed;
                            t_toodledo.Order = t_resolved.Order;
                            t_toodledo.Priority = t_resolved.Priority;
                            t_toodledo.ProjectId = t_resolved.ProjectId;
                            t_toodledo.Recurrence.AdvancedRepeat = t_resolved.Recurrence.AdvancedRepeat;
                            t_toodledo.Recurrence.Repeat = t_resolved.Recurrence.Repeat;
                            t_toodledo.UserId = t_resolved.UserId;

                            syncLogger.Info("Updating Task on Toodledo; Id: {0}", t.Id);
                            toodledoClient.UpdateTask(apiKey.Key, t);
                            if (errorOccurred) { logExitSync(); return; }
                        }

                    }
                    else
                    {
                        if (modifiedLocally)
                        {
                            syncLogger.Info("Updating Task on Toodledo; Id: {0}", t.Id);
                            toodledoClient.UpdateTask(apiKey.Key, t);
                            if (errorOccurred) { logExitSync(); return; }
                        }
                        if (modifiedOnline)
                        {
                            syncLogger.Info("Updating Task locally; Id: {0}", t.Id);
                            t.Collapsed = t_toodledo.Collapsed;
                            t.Content = t_toodledo.Content;
                            t.DueDate = t_toodledo.DueDate;
                            t.Indent = t_toodledo.Indent;
                            t.Completed = t_toodledo.Completed;
                            t.Order = t_toodledo.Order;
                            t.Priority = t_toodledo.Priority;
                            t.ProjectId = t_toodledo.ProjectId;
                            t.Recurrence.AdvancedRepeat = t_toodledo.Recurrence.AdvancedRepeat;
                            t.Recurrence.Repeat = t_toodledo.Recurrence.Repeat;
                            t.UserId = t_toodledo.UserId;
                        }
                    }

                }
                
                else
                {
                    if (!toodledoDeleted.Contains(t.Id))
                    {
                        syncLogger.Info("Adding Task to Toodledo; Id: {0}", t.Id);
                        toodledoClient.AddTask(apiKey.Key, t);
                        if (errorOccurred) { logExitSync(); return; }
                        
                        if (t.Completed) 
                        {
                            syncLogger.Info("Updating Task on Toodledo (InHistory={0}); Id: {1}", t.Completed, t.Id);
                            toodledoClient.UpdateTask(apiKey.Key, t); 
                        }

                        if (errorOccurred) { logExitSync(); return; }

                    }
                    else
                    {                        
                        if (modifiedLocally) 
                        {
                            syncLogger.Info("Readding Task to Toodledo (deleted, but modified locally); Id: {0}", t.Id);
                            toodledoClient.AddTask(apiKey.Key, t); 
                            if (errorOccurred) { logExitSync(); return; }
                            
                            if (t.Completed)
                            {
                                syncLogger.Info("Updating Task on Toodledo (InHistory={0}); Id: {1}" ,t.Completed, t.Id);
                                toodledoClient.UpdateTask(apiKey.Key, t);
                            }
                        }
                        else 
                        {
                            syncLogger.Info("Deleting Task locally; Id: {0}", t.Id);
                            TaskStore.RemoveTask(t.Id); 
                        }
                    }
                }
            }


            //remaining: Tasks added online
            var query = from task in toodledoTasksDict.Values
                        where !TaskStore.TaskExists(task.Id) && !localDeleted.Contains(task.Id)
                        select task;

            List<Task> addedTasks = query.ToList<Task>();

            foreach (Task t in addedTasks)
            {
                syncLogger.Info("Adding Task locally, Id: {0}", t.Id);
                TaskStore.AddTask(t);
            }
        }
        


        /// <summary>
        /// Logs that sync has been exited before being completed.
        /// </summary>
        private void logExitSync()
        {
            syncLogger.Error("sync() exited before Sync was completed");
        }

        /// <summary>
        /// Raises a new SyncConflictFound event.
        /// </summary>
        /// <param name="e">The SyncConflictEventArgs for this event.</param>
        private void OnSyncConflictFound(SyncConflictEventArgs e)
        {
            if (SyncConflictFound != null)
            {
                SyncConflictFound(this, e);
            }
        }    
    }
}
